Welcome to the BEHAV3D Tumor Profiler Demo. Here you can find a demo
example of how the Behaved Tumor Profiler pipeline works and what each
of the outputs can provide.
Tu run the pipeline for yourself, please refer to the Github
page, where you can find a wider explanation of the pipeline, as
well as the Google
Colab and R script versions.
Firstly all the necessary libraries must be installed
library(dplyr)
library(dtwclust)
library(e1071)
library(emmeans)
library(ggplot2)
library(gtools)
library(parallel)
library(pheatmap)
library(plotly)
library(plyr)
library(RANN)
library(readr)
library(reshape2)
library(scales)
library(sp)
library(spatstat)
library(stats)
library(tidyr)
library(umap)
library(viridis)
library(zoo)
We set a seed for reproducibility:
set.seed(123)
Input data can be obtained from image analysis software like Imaris (Oxford Instruments), where you extract the relevant features for each cell types. Each cell type must be inputted separately into the pipeline.
Input data must follow the following format:
Mouse_CellType_timelapse_LargeScaleRegion_Feature.csv
| Our analyzed Cell types | |||
|---|---|---|---|
| Tumor Cells | SR101 | MG (Microglia) | BV (Blood Vessel) |
Example:
2430F13_SR101_timelapse_CL3_2_Distance.csv
Mouse information includes: 2430 -> Mother |
F -> Sex | 13 -> Day
LargeScaleRegion information includes: CL2 -> Class
(Environmental Cluster) | 2430F13.CL2_2 or just
CL2_2 -> Position
These are the features to be uploaded:
DisplacementDistance_tumorDisplacement_Delta_LengthDisplacement_LengthPositionSpeedTime
The data is outputed by softwares like Imaris in a .csv
format.
For example, a Tumor cells 2430F11 CL1_1 displacement file would look like this (Each row represents a single cell track):
And a SR101 2430F16 CL1_1 Position file would look like this:
There are two ways to start or continue your analysis in BEHAV3D Tumor Profiler:
We need to import the dataset and classifying the information into
distinct dataframes
Create a reading function, that reads input from Imaris software:
## function to import the stats of the tracked tumor cells data from csv files extracted by Imaris
read_plus <- function(flnm) {
read_csv(flnm, skip = 3, col_types = cols()) %>%
mutate(filename = flnm)
}
Now we can import the datafiles of interest. We are basically extracting
the information from .csv files and creating the
corresponding dataframes
## set directory where csv files are located
library(plyr)
working_directory <- "D:/BEHAV3D_Tumor_Profiler/data/Tracked_tumor_cells"
setwd(working_directory)
# import volume per organoid object
pat = "*Displacement"
files <- list.files(path = working_directory, pattern = pat, full.names = T, recursive = TRUE)
files<-files[seq(1, length(files), 3)]
disp_csv <- ldply(files, read_plus)
# import distance to tumor
pat = "*Distance_tumor"
files <- list.files(path = working_directory, pattern = pat, full.names = T, recursive = TRUE)
dist_tumor_csv <- ldply(files, read_plus)
# import area per organoid object
pat = "*Displacement_Delta_Length"
files <- list.files(path = working_directory, pattern = pat, full.names = T, recursive = TRUE)
dipl_delta_csv <- ldply(files, read_plus)
# import area per organoid object
pat = "*Displacement_Length"
files <- list.files(path = working_directory, pattern = pat, full.names = T, recursive = TRUE)
dipl_length_csv <- ldply(files, read_plus)
# import position per organoid object
pat = "*Position"
files <- list.files(path = working_directory, pattern = pat, full.names = T, recursive = TRUE)
pos_csv <- ldply(files, read_plus)
# import speed
pat = "*Speed"
files <- list.files(path = working_directory, pattern = pat, full.names = T, recursive = TRUE)
speed_csv <- ldply(files, read_plus)
# import Time
pat = "*Time"
files <- list.files(path = working_directory, pattern = pat, full.names = T, recursive = TRUE)
time_csv <- ldply(files, read_plus)
### ## make each ID unique (IDs imported from different imaging files can be repeated):
category <- as.factor(dist_tumor_csv$filename)
ranks <- rank(-table(category), ties.method="first")
ranks <- as.data.frame(ranks)
ranks$filename <- row.names(ranks)
dist_tumor_csv <- left_join(dist_tumor_csv, ranks)
dist_tumor_csv$ID2 <- with(dist_tumor_csv, interaction(ID, ranks))
category <- as.factor(pos_csv$filename)
ranks <- rank(-table(category), ties.method="first")
ranks <- as.data.frame(ranks)
ranks$filename <- row.names(ranks)
pos_csv <- left_join(pos_csv, ranks)
pos_csv$ID2 <- with(pos_csv, interaction(ID, ranks))
category <- as.factor(time_csv$filename)
ranks <- rank(-table(category), ties.method="first")
ranks <- as.data.frame(ranks)
ranks$filename <- row.names(ranks)
time_csv <- left_join(time_csv, ranks)
time_csv$ID2 <- with(time_csv, interaction(ID, ranks))
category <- as.factor(speed_csv$filename)
ranks <- rank(-table(category), ties.method="first")
ranks <- as.data.frame(ranks)
ranks$filename <- row.names(ranks)
speed_csv <- left_join(speed_csv, ranks)
speed_csv$ID2 <- with(speed_csv, interaction(ID, ranks))
category <- as.factor(dipl_length_csv$filename) ##this measures how much distnace did a cell move over, a cell can mover over a big distance and stay at the same position
ranks <- rank(-table(category), ties.method="first")
ranks <- as.data.frame(ranks)
ranks$filename <- row.names(ranks)
dipl_length_csv <- left_join(dipl_length_csv, ranks)
dipl_length_csv$ID2 <- with(dipl_length_csv, interaction(ID, ranks))
category <- as.factor(dipl_delta_csv$filename) ## this variable measures how much distance did the cell move from its origin
ranks <- rank(-table(category), ties.method="first")
ranks <- as.data.frame(ranks)
ranks$filename <- row.names(ranks)
dipl_delta_csv <- left_join(dipl_delta_csv, ranks)
dipl_delta_csv$ID2 <- with(dipl_delta_csv, interaction(ID, ranks))
category <- as.factor(disp_csv$filename) ## a bit similar to the previous ones, this measures the area that a cell covers overtime
ranks <- rank(-table(category), ties.method="first")
ranks <- as.data.frame(ranks)
ranks$filename <- row.names(ranks)
disp_csv <- left_join(disp_csv, ranks)
disp_csv$ID2 <- with(disp_csv, interaction(ID, ranks))
Firstly, we create the dataframe containing the statistics of
interest
detach(package:plyr)
##create dataframe containing the statistics of interest
master_1 <- left_join(dist_tumor_csv[,c(1,13)],time_csv[,c(1,5,6,8,10,11)],
by=c("ID2" = "ID2"))
master_1 <- left_join(master_1,disp_csv[,c(1,11)],
by=c("ID2" = "ID2"))
master_1 <- left_join(master_1,dipl_delta_csv[,c(1,11)],
by=c("ID2" = "ID2"))
master_1 <- left_join(master_1,dipl_length_csv[,c(1,11)],
by=c("ID2" = "ID2"))
master_1 <- left_join(master_1,speed_csv[,c(1,11)],
by=c("ID2" = "ID2"))
master <- left_join(master_1,pos_csv[,c(1,2,3,14)],
by=c("ID2" = "ID2"))
colnames(master) <- c("dist_tumor","ID2","Time","Tracked","TrackID", "filename","ranks","disp2","disp_d","disp_l","speed","x","y","z")
# Extract mouse data (ID) using regexp (mother+sex+day)
master$mouse <- master$filename
master$mouse <- gsub("^.*[_/](\\d*[MF]\\d+).*", "\\1", master$mouse, perl=TRUE)
# Extract position metadata
master$position <- master$filename
master$position <- gsub(".*_(CL\\d+_\\d+)_.*", "\\1", master$position, perl=TRUE)
master$position<-with(master, interaction(mouse, position))
# Extract class metadata
master$class <- master$position
master$class <- gsub(".*(CL\\d+)_.*", "\\1", master$class, perl=TRUE)
master$filename<-NULL
## Round the time
master$Time<-round(master$Time, digits=2)
##check that the imported tracks look ok
g<-ggplot(master)+geom_path(aes(x, y, group=TrackID), size=0.2,
arrow = arrow(ends = "last",type = "open",length = unit(0.01, "inches")))+
theme_bw()+theme(aspect.ratio = 1)+facet_wrap(class~position,scales = "free")
g
, the Blood Vessel information is extracted:
###Import BLOOD VESSELS stats (does not exist for all tracked positions so needs to be processed separtely.)
working_directory2 <- "D:/BEHAV3D_Tumor_Profiler/data/BV_stats"
setwd(working_directory2)
files <- list.files(path = working_directory2, full.names = T, recursive = TRUE)
BV_df <- ldply(files, read_plus)
###Process blood vessels data:
BV_df<-BV_df[c(1,6,7,8,9,11)]
colnames(BV_df) <- c("dist_BV","Time","Tracked","TrackID", "ID","filename")
## rename mouse accroding to filename:
BV_df$mouse<-BV_df$filename
BV_df$mouse <- gsub("^.*[_/](\\d*[MF]\\d+).*", "\\1", BV_df$mouse, perl=TRUE)
BV_df$position <- BV_df$filename
BV_df$position <- gsub(".*_(CL\\d+_\\d+)_.*", "\\1", BV_df$position, perl=TRUE)
BV_df$position <- with(BV_df, interaction(mouse, position))
BV_df$class <- BV_df$position
BV_df$class <- gsub(".*(CL\\d+)_.*", "\\1", BV_df$class, perl=TRUE)
## create ranks column based on master ranks:
ranks_master<-subset(master, position %in% BV_df$position)
ranks_master<-ranks_master[!duplicated(ranks_master$position),c(6,15)]
colnames(ranks_master)[1]<-"ranks_bv"
BV_df<- left_join(BV_df,ranks_master)
## keep only tracked cells and with a TrackID:
BV_df<-subset(BV_df, Tracked=="Tracked")
## remove any NA:
BV_df<-na.omit(BV_df)
## create unique Track_ID for Blood vessels dataframe:
BV_df$Track2 <- with(BV_df, interaction(ranks_bv, TrackID))
BV_df$Track2 <- gsub(".", '', BV_df$Track2, fixed = T)
BV_df$Track2 <- as.numeric(as.character(BV_df$Track2))
##sumarize per TrackID the average, min and max distance to blood vessels
ggplot(BV_df,aes(dist_BV))+geom_histogram()+facet_wrap(.~ position)
BV_df<-na.omit(BV_df)
BV_df$BV_contact<-ifelse(BV_df$dist_BV<4, 0, 1)
BV_df_sum<-BV_df%>%group_by(Track2)%>%summarise(BV_sd=sd(dist_BV)/sqrt(length(dist_BV)),BV_mean=mean(dist_BV), BV_min=min(dist_BV), BV_max=max(dist_BV), BV_contact=mean(BV_contact))
library(plyr)
##import Sr101 cells for the populations that have them
working_directory <- "D:/BEHAV3D_Tumor_Profiler/data/SR101_objects"
setwd(working_directory)
# import volume per organoid object
# import position per organoid object
pat = "*Position"
files <- list.files(path = working_directory, pattern = pat, full.names = T, recursive = TRUE)
pos_csv <- ldply(files, read_plus)
# import Time
pat = "*Time"
files <- list.files(path = working_directory, pattern = pat, full.names = T, recursive = TRUE)
time_csv <- ldply(files, read_plus)
### ## make each TrackID unique (TrackIDs imported from different imaging files can be repeated):
category <- as.factor(pos_csv$filename)
ranks <- rank(-table(category), ties.method="first")
ranks <- as.data.frame(ranks)
ranks$filename <- row.names(ranks)
pos_csv <- left_join(pos_csv, ranks)
pos_csv$ID2 <- with(pos_csv, interaction(ID, ranks))
category <- as.factor(time_csv$filename)
ranks <- rank(-table(category), ties.method="first")
ranks <- as.data.frame(ranks)
ranks$filename <- row.names(ranks)
time_csv <- left_join(time_csv, ranks)
time_csv$ID2 <- with(time_csv, interaction(ID, ranks))
detach(package:plyr)
##create dataframe containing the statistics of interest
master_SR101 <- left_join(pos_csv[,c(1,2,3,10,12)],time_csv[,c(1,9)],
by=c("ID2" = "ID2"))
colnames(master_SR101) <- c("x","y","z","filename","ID2","Time")
## rename mouse accroding to filename:
# Extract mouse data (ID) using regexp (mother+sex+day)
master_SR101$mouse <- master_SR101$filename
master_SR101$mouse <- gsub("^.*[_/](\\d*[MF]\\d+).*", "\\1", master_SR101$mouse, perl=TRUE)
# Extract position metadata
master_SR101$position <- master_SR101$filename
master_SR101$position <- gsub(".*_(CL\\d+_\\d+)_.*", "\\1", master_SR101$position, perl=TRUE)
master_SR101$position<-with(master_SR101, interaction(mouse, position))
# Extract class metadata
master_SR101$class <- master_SR101$position
master_SR101$class <- gsub(".*(CL\\d+)_.*", "\\1", master_SR101$class, perl=TRUE)
master_SR101$filename<-NULL
## Convert imported time to hours:
master_SR101$Time<-round(master_SR101$Time, digits=2)
##import MG cells for the populations that have them
library(plyr)
working_directory <- "D:/BEHAV3D_Tumor_Profiler/data/MG_objects"
setwd(working_directory)
# import position per organoid object
pat = "*Position"
files <- list.files(path = working_directory, pattern = pat, full.names = T, recursive = TRUE)
pos_csv <- ldply(files, read_plus)
# import Time
pat = "*Time"
files <- list.files(path = working_directory, pattern = pat, full.names = T, recursive = TRUE)
time_csv <- ldply(files, read_plus)
### ## make each TrackID unique (TrackIDs imported from different imaging files can be repeated):
category <- as.factor(pos_csv$filename)
ranks <- rank(-table(category), ties.method="first")
ranks <- as.data.frame(ranks)
ranks$filename <- row.names(ranks)
pos_csv <- left_join(pos_csv, ranks)
pos_csv$ID2 <- with(pos_csv, interaction(ID, ranks))
category <- as.factor(time_csv$filename)
ranks <- rank(-table(category), ties.method="first")
ranks <- as.data.frame(ranks)
ranks$filename <- row.names(ranks)
time_csv <- left_join(time_csv, ranks)
time_csv$ID2 <- with(time_csv, interaction(ID, ranks))
detach(package:plyr)
##create dataframe containing the statistics of interest
master_MG <- left_join(pos_csv[,c(1,2,3,10,12)],time_csv[,c(1,9)],
by=c("ID2" = "ID2"))
colnames(master_MG) <- c("x","y","z","filename","ID2","Time")
## rename mouse accroding to filename:
# Extract mouse data (ID) using regexp (mother+sex+day)
master_MG$mouse<-master_MG$filename
master_MG$mouse <- gsub("^.*[_/](\\d*[MF]\\d+).*", "\\1", master_MG$mouse, perl=TRUE)
# Extract position metadata
master_MG$position <- master_MG$filename
master_MG$position <- gsub(".*_(CL\\d+_\\d+)_.*", "\\1", master_MG$position, perl=TRUE)
master_MG$position<-with(master_MG, interaction(mouse, position))
# Extract class metadata
master_MG$class <- master_MG$position
master_MG$class <- gsub(".*(CL\\d+)_.*", "\\1", master_MG$class, perl=TRUE)
master_MG$filename<-NULL
## Convert imported time to hours:
master_MG$Time<-round(master_MG$Time, digits=2)
Now we need to join the information from all of the sources into a
single dataframe
We first calulate the interactions between the nearest cells:
### test that tracks imported are fine:
master_test<-master%>%group_by(position)%>%mutate(x=x-min(x), y=y-min(y))
g<-ggplot(master)+geom_path(aes(x, y, group=TrackID, colour=dist_tumor), arrow = arrow(ends = "last", type = "closed",length = unit(0.005, "inches")))+theme_bw()
g<-g + theme(legend.position = "none")+facet_wrap(.~position, scales = "free")
g
## To calculate interactions between cells
List2=list()
for (m in unique(master$position)){
distance_M4<-master[which(master$position==m),]
List = list()
for (i in unique(distance_M4$Time)){
distancei <- distance_M4[distance_M4$Time==i,]
distancei2<-as.data.frame(distancei[,c(3,11,12,13)])
coordin<-ppx(distancei2, coord.type=c("t","s","s", "s"))
dist1<- nndist(coordin)
dist2<- nndist(coordin, k=2)
dist3<- nndist(coordin, k=3)
dist4<- nndist(coordin, k=4)
dist5<- nndist(coordin, k=5)
dist6<- nndist(coordin, k=6)
dist7<- nndist(coordin, k=7)
dist8<- nndist(coordin, k=8)
dist9<- nndist(coordin, k=9)
dist10<- nndist(coordin, k=10)
dist<-cbind(dist1,dist2,dist3, dist4,dist5, dist6, dist7, dist8, dist9, dist10) ### calculate distance to the three nearest neighbors
dist<-data.frame(rowMeans(dist[,c(1:3)]),rowMeans(dist))
distanceDFi_dist<-cbind(distancei, dist)
List[[length(List)+1]] <-distanceDFi_dist ## store to list object
}
master_dist <- do.call(rbind, List) ## convert List to dta
List2[[length(List2)+1]] <-master_dist
}
master_distance<-do.call(rbind, List2)
colnames(master_distance)[c(17,18)] <- c("dist_3_neigh", "dist_10_neigh")
###remove the untracked tracks:
master_distance<-subset(master_distance, Tracked=="Tracked")
## create a unique track_ID for master:
master_distance$Track2 <- with(master_distance, interaction(ranks, TrackID))
master_distance$Track2 <- gsub(".", '', master_distance$Track2, fixed = T)
master_distance$Track2 <- as.numeric(as.character(master_distance$Track2))
##check that the imported tracks look ok
g<-ggplot(master_distance)+geom_path(aes(x, y, group=Track2), size=0.2,
arrow = arrow(ends = "last",type = "open",length = unit(0.01, "inches")))+
theme_bw()+theme(aspect.ratio = 1)+facet_wrap(class~position,scales = "free")
g
##For SR101
master_distance_SR101<-subset(master_distance, mouse %in% unique(master_SR101$mouse))
### Calculate distance from Tumor cells to SR101.
### Considering that olig2 doens;t move much, let's project the position of the birghtest timepoint from each movie to all timepoints.
### Identify birghtest timepoint by the number of SR101 objects:
brightest_SR101<-master_SR101%>%group_by(Time, position)%>%summarise(n=n())%>%group_by( position)%>%filter(n==max(n))
master_SR101<-subset(master_SR101, Time %in%brightest_SR101$Time)
List2<-list()
for (m in unique(master_distance_SR101$position)){
master_pos <-master_distance_SR101%>%filter(position==m)%>%group_by(Time)%>%arrange(Time)
master_SR101_pos <-master_SR101%>%filter(position==m)%>%group_by(Time)%>%arrange(Time)
List1 = list() ## create list for ID of the contacting objects
for(t in unique(master_pos$Time)){
master_pos_t <-master_pos%>%filter(Time==t)
### calculate distance to 5 neigrest neighorbing SR101 in a radius of 40um
cells_radius<-nn2(data=master_SR101_pos[c("x","y","z")], query = master_pos_t[c("x","y","z")],k=30, treetype = 'bd',searchtype = "radius", radius = 30)
cells_min<-nn2(data=master_SR101_pos[c("x","y","z")], query = master_pos_t[c("x","y","z")],k=1, treetype = 'bd',searchtype = "standard")
dist_table = data.frame(master_pos_t[c("Time","Track2","mouse","position","class")],cells_min[["nn.dists"]],cells_radius[["nn.dists"]])
List1[[length(List1)+1]] <-dist_table
rm(temp)
}
cell_radius_t <- do.call(rbind, List1)
List2[[length(List2)+1]] <-cell_radius_t
rm(cell_radius_t)
}
master_dist_SR101 <- do.call(rbind, List2)
### convert the distant cells to 0
temp2<-master_dist_SR101[c("X1","X2","X3","X4","X5", "X6", "X7", "X8", "X9", "X10", "X11","X12","X13","X14","X15","X16","X17","X18","X19","X20", "X21","X22","X23","X24","X25","X26","X27","X28","X29","X30")]
temp2[temp2 >1.340781e+153]<-0
### Calculate number of contacts:
master_dist_SR101$n_SR101<-rowSums(temp2 != 0)
### renamen min_SR101:
colnames(master_dist_SR101)[colnames(master_dist_SR101) == "cells_min...nn.dists..."] <- "min_SR101"
### bind information on SR101 distnaces to the main dataframe:
master_dist_SR101[c("X1","X2","X3","X4","X5", "X6", "X7", "X8", "X9", "X10", "X11","X12","X13","X14","X15","X16","X17","X18","X19","X20", "X21","X22","X23","X24","X25","X26","X27","X28","X29","X30")]<-NULL
## For each Track2 find what is the minimal distance to SR101 and the max n of SR101 neiighbors in 30um diameter:
master_distance_SR101<-master_dist_SR101%>%group_by(Track2, mouse, position, class)%>%summarize(n_SR101=max(n_SR101), min_SR101 =min(min_SR101))
### plot per position types the values of
p <- ggplot(master_distance_SR101)+geom_jitter(aes(x=class, y=n_SR101))+
geom_bar(aes(x=class , y=n_SR101),alpha=0.5,stat = "summary") +ggtitle("number of neighboring SR101 cells per position type")+theme_classic()+
theme(aspect.ratio=0.7)
p
p <- ggplot(master_distance_SR101)+geom_jitter(aes(x=class, y=min_SR101))+
geom_boxplot(aes(x=class , y=min_SR101),alpha=0.5, outlier.colour = NA) +ggtitle("min distance to SR101 cells per position type")+theme_classic()+
theme(aspect.ratio=0.7)
p
##For MG
master_distance_MG<-subset(master_distance, mouse %in% unique(master_MG$mouse))
### Calculate distance from Tumor cells to MG.
### Considering that olig2 doens;t move much, let's project the position of the birghtest timepoint from each movie to all timepoints.
### Identify birghtest timepoint by the number of MG objects:
brightest_MG<-master_MG%>%group_by(Time, position)%>%summarise(n=n())%>%group_by( position)%>%filter(n==max(n))
master_MG<-subset(master_MG, Time %in%brightest_MG$Time)
List2<-list()
for (m in unique(master_distance_MG$position)){
master_pos <-master_distance_MG%>%filter(position==m)%>%group_by(Time)%>%arrange(Time)
master_MG_pos <-master_MG%>%filter(position==m)%>%group_by(Time)%>%arrange(Time)
List1 = list() ## create list for ID of the contacting objects
for(t in unique(master_pos$Time)){
master_pos_t <-master_pos%>%filter(Time==t)
### calculate distance to 5 neigrest neighorbing MG in a radius of 30um
cells_radius<-nn2(data=master_MG_pos[c("x","y","z")], query = master_pos_t[c("x","y","z")],k=30, treetype = 'bd',searchtype = "radius", radius = 30)
cells_min<-nn2(data=master_MG_pos[c("x","y","z")], query = master_pos_t[c("x","y","z")],k=1, treetype = 'bd',searchtype = "standard")
dist_table = data.frame(master_pos_t[c("Time","Track2","mouse","position","class")],cells_min[["nn.dists"]],cells_radius[["nn.dists"]])
List1[[length(List1)+1]] <-dist_table
rm(temp)
}
cell_radius_t <- do.call(rbind, List1)
List2[[length(List2)+1]] <-cell_radius_t
rm(cell_radius_t)
}
master_dist_MG <- do.call(rbind, List2)
### convert the distant cells to 0
temp2<-master_dist_MG[c("X1","X2","X3","X4","X5", "X6", "X7", "X8", "X9", "X10", "X11","X12","X13","X14","X15","X16","X17","X18","X19","X20", "X21","X22","X23","X24","X25","X26","X27","X28","X29","X30")]
temp2[temp2 >1.340781e+153]<-0
### Calculate number of contacts:
master_dist_MG$n_MG<-rowSums(temp2 != 0)
### renamen min_MG:
colnames(master_dist_MG)[colnames(master_dist_MG) == "cells_min...nn.dists..."] <- "min_MG"
### bind information on MG distnaces to the main dataframe:
master_dist_MG[c("X1","X2","X3","X4","X5", "X6", "X7", "X8", "X9", "X10", "X11","X12","X13","X14","X15","X16","X17","X18","X19","X20", "X21","X22","X23","X24","X25","X26","X27","X28","X29","X30")]<-NULL
## For each Track2 find what is the minimal distance to MG and the max n of MG neiighbors in 30um diameter:
master_distance_MG<-master_dist_MG%>%group_by(Track2, mouse, position, class)%>%summarize(n_MG=max(n_MG), min_MG =min(min_MG))
### plot per position types the values of
p <- ggplot(master_distance_MG)+geom_jitter(aes(x=class, y=n_MG))+
geom_bar(aes(x=class , y=n_MG),alpha=0.5,stat = "summary") +ggtitle("number of neighboring MG cells per position type")+theme_classic()+
theme(aspect.ratio=0.7)
p
p <- ggplot(master_distance_MG)+geom_jitter(aes(x=class, y=min_MG))+
geom_boxplot(aes(x=class , y=min_MG),alpha=0.5, outlier.colour = NA) +ggtitle("min distance to MG cells per position type")+theme_classic()+
theme(aspect.ratio=0.7)
p
Time of interest selection and substitution of NA values:
### create empyt dataframe with combination of Time of interest:
max(master_distance$Time)
## [1] 5.44
length(seq(from=0, to=5.6, by=0.2))
## [1] 29
empty_df = master_distance[1:29,] ### create a dataframe with the same size as the sequence of Time of interest:
empty_df[!is.na(empty_df)] <- NA ## make it empyt
empty_df$Time<-seq(from=0, to=5.6, by=0.2)
empty_df$Track2<-0.5
### now merge this fake dataframe with my dataframe of interest:
master_distance<-rbind(master_distance, empty_df)
### complete all the datasets so that they have the same timepoints:
master_distance<-master_distance%>%complete(Track2,Time)
### remove the fake track that is not necessary anymore:
master_distance<-subset(master_distance, !Track2==0.5)
##round the number, otherwise they are not equal
master_distance$Time<-round(master_distance$Time,2)
##if there is any track2 and time that was duplicated remove the NA
# Assuming your dataframe is named your_dataframe
master_distance <- master_distance %>%
group_by(Time, Track2) %>%
dplyr::slice(if (n() == 1) 1 else which(!is.na(dist_tumor)))
### create a for loop to refill all the NA with interpolated values:
### create a list of the columns names that need to be refilled
column_names<-names(master_distance)
column_names<-subset(column_names,!column_names %in%c("TrackID","Track2", "ranks","ID","ID2","Time","position","mouse","class","pos", "Tracked","speed"))
## create a first dataset with refilled values for speed:
time_series<-acast(master_distance, Time ~ Track2, value.var='speed',fun.aggregate = mean)
## rownames timepoints:
row.names(time_series)<-unique(master_distance$Time)
## get rid of NA
time_series_zoo<-zoo(time_series, row.names(time_series))
time_series_zoo<-na.approx(time_series_zoo) ## replace by interpolated value
time_series<-as.matrix(time_series_zoo)
time_series2<-melt(time_series)
data<-time_series2[complete.cases(time_series2), ]
colnames(data)<-c("Time", "Track2", "speed")
### ----------
for (i in column_names){
time_series<-acast(master_distance, Time ~ Track2, value.var=i,fun.aggregate = mean)
row.names(time_series)<-unique(master_distance$Time)
## get rid of NA
time_series_zoo<-zoo(time_series,row.names(time_series))
time_series_zoo<-na.approx(time_series_zoo) ## replace by last value
time_series<-as.matrix(time_series_zoo)
time_series2<-melt(time_series)
new<-time_series2[complete.cases(time_series2), ]
data[ , ncol(data) + 1] <- new[3] # Append new column
colnames(data)[ncol(data)] <- paste0(i)
}
### check that the data is correct by plotting the evolution of a certain variable overtime -----
p1 <-ggplot(data, aes(Time, disp2, group=Track2, color = as.factor(Track2))) +
geom_smooth(method = "loess",size = 0.5, se = F, alpha=0.3, span=1)+
theme_bw() +
theme(axis.text.x = element_text(size=10), axis.text.y = element_text(size=5), axis.title.y = element_text(size = 10), axis.title.x = element_text(size = 15), legend.text=element_text(size= 10))+
ggtitle("disp2")+theme(aspect.ratio=1,legend.position = "none")
p1
Then, metadata information is added to the analysis:
### add metadata info
master_cor <- data
master_metadata<- master_distance[c("position","mouse","class", "Track2")]
master_metadata<-na.omit(master_metadata)
master_metadata<-master_metadata[!duplicated(master_metadata$Track2),]
#detach(package:plyr, unload = TRUE)
master_cor<- left_join(master_cor ,master_metadata)
### Filter timepoints of interest with same time interval:
time_of_interest<-as.character(seq(from=0, to=5.6, by=0.2)) ### convert to character
master_cor$Time<-as.character(master_cor$Time)
master_cor<-subset(master_cor, Time%in%time_of_interest)
master_cor$Time<-as.numeric(master_cor$Time) ### now back to numeric
##check that the imported tracks look ok
g<-ggplot(master_cor)+geom_path(aes(x, y, group=Track2), size=0.2,
arrow = arrow(ends = "last",type = "open",length = unit(0.01, "inches")))+
theme_bw()+theme(aspect.ratio = 1)+facet_wrap(class~position,scales = "free")
g
Finally, the generated dataframes are exported to use as
Checkpoints when repeating the analysis:
Set working directory to export dataframes
setwd("D:/BEHAV3D_Tumor_Profiler/results/rds")
All imported information:
saveRDS(master_distance, "master_distance.rds")
Time of interest subset information:
saveRDS(master_cor,"master_cor.rds")
Microglia information:
saveRDS(master_distance_MG,"master_distance_MG.rds")
SR101 information:
saveRDS(master_distance_SR101,"master_distance_SR101.rds")
Blood Vessel information:
saveRDS(BV_df_sum,"BV_df_sum.rds")
.rds files obtained.If the RDS files are in the working directory, the BEHAV3D Tumor
Profiler script will skip the data input and initial pre-processing, by
using the provided .rds files.
setwd("D:/BEHAV3D_Tumor_Profiler/results")
master_cor<-readRDS("master_cor.rds")
master_distance<-readRDS("master_distance.rds")
master_distance_MG<-readRDS("master_distance_MG.rds")
master_distance_SR101<-readRDS("master_distance_SR101.rds")
BV_df_sum<-readRDS("BV_df_sum.rds")
print("data already imported")
## [1] "data already imported"
After the .rds checkpoints, we can continue with joint
data processing.
In this section, the joint dataframes are adjusted so all track have the same length
### sumarize the length of the tracks:
## create relative time
master_cor2<-master_cor %>%
group_by(Track2) %>%arrange(Time)%>%mutate(Time2 = Time - dplyr::first(Time))
ggplot(master_cor2,aes(Time2))+geom_histogram()+facet_wrap(.~ position)
This plot shows a summary of the imported tracks for each position and
time
max_time<-master_cor2%>%group_by(position)%>%summarise(max_Time=max(Time2))
min_time <- min(max_time$max_Time) #this is the minimal time that any position has
hist(max_time$max_Time)
Here we can see the track length distribution, so we can normalize all tracks to the minimal common time:
master_cor2<-master_cor2 %>%
group_by(Track2)%>%filter(Time2<min_time) ##Minimal time for a position is 2.6
## keep only the tracks that are of the same length (max number of timepoints (13 in this case), this is how they were defined)
# Calculate the most frequent n for each group
subtrack_length<-master_cor2%>%group_by(Track2, position)%>%
summarise(n=n_distinct(Time2), first_t=min(Time2), last_t=max(Time2))%>%
ungroup()
hist(subtrack_length$n)
This histogram shows the track length across all tracks, so the most
common track length can be selected
freq_n <- as.numeric(which.max(table(subtrack_length$n))) # Most frequent n
subtrack_length<-subtrack_length%>%filter(n==freq_n) # we need to automate this to get the most frequent n
#Now we only keep these subtracks that have the same length:
master_cor2<-subset(master_cor2, Track2%in%subtrack_length$Track2)
Plot the unique tracks for each position using the tumor distance as
color label:
### create a parameter for distance to tumor core
master_cor2<-master_cor2 %>%
group_by(position)%>%mutate(dist_tumor_1 = scales::rescale(dist_tumor, to=c(0,100)))
### normalize the distance to tumor factor per position position
master_cor2<-master_cor2 %>%
group_by(Track2) %>%arrange(Time2)%>%mutate(dist_tumor = dist_tumor - dplyr::first(dist_tumor))%>%ungroup()
#### check tracks
master_M4_test<-master_cor2%>%group_by(position, mouse)%>%mutate(x=x-min(x), y=y-min(y))
g<-ggplot(master_M4_test)+geom_path(aes(x, y, group=Track2, colour=dist_tumor), arrow = arrow(ends = "last", type = "closed",length = unit(0.005, "inches")))+theme_bw()
g<-g + theme(legend.position = "none")+scale_colour_gradient2(low = "blue", mid = "grey" , high = "red") +facet_wrap(.~position)
g
Number of unqiue tracks that are processed:
length(unique(master_cor2$Track2))
## [1] 981
Now we need to do some feature engineering, since we make a sliding
window analysis we need to requantify the values of displacement, speed,
etc from scratch
## scaling
library(parallel)
library(dplyr)
library(dtwclust)
library(stats)
library(scales)
#detach(package:spatstat, unload = TRUE)
###normalize the data:
master_norm<-master_cor2%>%ungroup()
column_names2<-names(master_cor2)
## select what data to normalize
column_names2<-subset(column_names2,!column_names2 %in%c("Time","Track2","x","y","z","position","mouse","class","Time2", "dist_tumor_1", "dist_3_neigh", "dist_10_neigh", "iteration", "Track2"))
master_norm<-as.data.frame(master_norm)
# transform skwed variables
# Calculate skewness for each variable
skew_values <- apply(master_norm[c(column_names2)], 2, skewness)
# Identify variables with skewness greater than a threshold (e.g., 0.5)
skewed_variables <- names(skew_values[abs(skew_values) > 2])
# Apply logarithmic transformation to skewed variables
master_norm[ , skewed_variables] <- log1p(master_norm[ , skewed_variables])
We performa a PCA dimensionality reduction to prepare for the
multivariate analysis
### perfrom PCA on the factors I want to use
master_scaled<-master_norm[c(column_names2)]%>%mutate(across(everything(), scale))%>%ungroup() ##first scale
##now to give more weight to the varibale of dist_tumor so the ability of cells to leave
#master_scaled[,c("dist_tumor")]<-master_scaled[,c("dist_tumor")]*2
master_pca<-prcomp(master_norm[c(column_names2)], scale = T)
master_pca2<-master_pca[["x"]] ##select only PC
master_pca3<-data.frame(master_norm[,c(2)],master_pca2[,c(1:3)]) ### take only first 3 components since they explain most variation
colnames(master_pca3)[1]<- "Track2"
Plot the PCA information:
pcaCharts <- function(x) {
x.var <- x$sdev ^ 2
x.pvar <- x.var/sum(x.var)
print("proportions of variance:")
print(x.pvar)
par(mfrow=c(2,2))
plot(x.pvar,xlab="Principal component", ylab="Proportion of variance explained", ylim=c(0,1), type='b')
plot(cumsum(x.pvar),xlab="Principal component", ylab="Cumulative Proportion of variance explained", ylim=c(0,1), type='b')
screeplot(x, main="Absolute Variance", xlab="Principal Component")
screeplot(x,type="l", main="Absolute Variance")
par(mfrow=c(1,1))
}
pcaCharts(master_pca)
## [1] "proportions of variance:"
## [1] 0.5643365414 0.1929346874 0.1787666118 0.0631326010 0.0008295584
Once we have normalized, adjusted and prepared all the data for analysis, it is time to proceed to the analysis modules.
The BEHAV3D Tumor Profiler is divided in three distinct modules:
1. Heterogeneity Module - Implements multiparametric single-cell time-series classification, allowing us to identify distinct single-cell behavioral patterns
Optional modules:
2. Large-scale phenotyping module - Performs large-scale TME phenotyping and identifies regions with a specific cellular composition and architecture within the TME of intravitally imaged tumors
3. Small-scale phenotyping module - Further refines TME phenotyping to better understand tumor cell behavior
The optional modules are already including the necessary sections for module 1 to allow for an easy run.
Modules are collapsed to facilitate navigation. Please unscroll the modules you would like to check
Dynamic Time Warping (DTW) is an algorithm used to measure similarity between two time series by aligning them in a way that minimizes the distance between corresponding points. It is particularly useful for comparing time series that may vary in speed or timing, like on this case.
If the matrix_distmat.rds file exists, it will skip the
DTW analysis time, as it has been run previously. Else, it will run
normally and generate the file at the end.
###MULTIVARIATE
list_master_pca <- split(master_pca3[,-c(1)],master_pca3$Track2) ## split into list
if ( ! file.exists("matrix_distmat.rds")){
##parallel working
# load parallel
library(parallel)
# create multi-process workers
workers <- makeCluster(detectCores()-2)
# load dtwclust in each one, and make them use 1 thread per worker
invisible(clusterEvalQ(workers, {
library(dtwclust)
RcppParallel::setThreadOptions(1L)
}))
# register your workers, e.g. with doParallel
require(doParallel)
registerDoParallel(workers)
###MULTIVARIATE
set.seed(123)
distmat <- proxy::dist(list_master_pca, method = "dtw")
saveRDS(distmat, file = "distmat.rds")
matrix_distmat<-as.matrix(distmat)
saveRDS(matrix_distmat, file = "matrix_distmat.rds")
}else{
matrix_distmat <- readRDS("matrix_distmat.rds")
}
Track2<-as.numeric(names(list_master_pca))
Now, we can perform the UMAP representation and cluster in the desired number of clusters. Any of the parameters can be modified to your convenience
# Adjust parameters according to the experiment characteristics
umap_dist<- umap(matrix_distmat,n_components=2,input="dist",init = "random",
n_neighbors=7,min_dist=0.5, spread=6,n_epochs=1000 , local_connectivity=1)
umap_1 <- as.data.frame(umap_dist$`layout`)
#scatter3Drgl(umap_1$V1, umap_1$V2, umap_1$V3)
Plot of the raw UMAP
plot(umap_dist$`layout`, # Plot the result
col=rgb(0,0,0,alpha=0.1),
pch=19,
asp=0.4, xlab="UMAP 1", ylab="UMAP 2",
main="Raw UMAP")
Here, we determine the number of clusters for the analysis.
(n_cluster)
umap_1 <- as.data.frame(umap_dist$`layout`)
Track2_umap<-cbind(Track2,umap_1)
positiontype<-master_norm[,c("Track2", "position","class","mouse")]
positiontype<- positiontype[!duplicated(positiontype$Track2),]
umap_2 <- left_join(Track2_umap ,positiontype)
# Clustering
n_cluster=7
hc <- hclust(dist(umap_dist$`layout`, method = "euclidean"), method = "ward.D2")
hierarchical_clusters <- cutree(hc, k = n_cluster)
umap_3 <- cbind(hierarchical_clusters, umap_2)
colnames(umap_3)[1]<- "cluster2"
Here. we can obtain different UMAP representations, providing our color
palette
##plot
# Make a color palette that adjusts to the cluster direction
# Generate a continuous palette with 100 colors
continuous_palette <- colorRampPalette(c("cyan4", "darkturquoise","darkorchid4","darkorchid1", "deeppink4","deeppink1", "goldenrod3", "gold"))(50)
# Select 8 colors from the continuous palette
mypalette_1<- continuous_palette[seq(1, length(continuous_palette), length.out = n_cluster)]
ggplot(umap_3, aes(x=V1, y=V2, color=as.factor(cluster2))) +
geom_point(size=2, alpha=0.8) + labs(color="cluster")+
xlab("") + ylab("")+
ggtitle("umap Cluster") +scale_color_manual(values=mypalette_1)+
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)
UMAP positions colored by large-scale regions
ggplot(umap_3, aes(x=V1, y=V2, color=as.factor(class))) +
geom_point(size=0.5, alpha=0.8) + labs(color="Environment_cluster")+
xlab("") + ylab("")+
ggtitle("umap Cluster") +scale_color_manual(values=c("cyan","gold","red"))+
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+facet_wrap(mouse~position)
UMAP colored by large-scale regions
ggplot(umap_3, aes(x=V1, y=V2, color=as.factor(class))) +
geom_point(size=2, alpha=0.6) + labs(color="Tumor region phenotype")+
xlab("") + ylab("")+
ggtitle("distribution of large scale regions")+scale_color_manual(values=c("cyan","gold","red"))+
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)
### UMAP direction
Here, we incorporate the MG and SR101 information to project the direction information into the UMAP
### calculate average stats per track
master_class <- left_join(master_cor2 ,umap_3[c(1:4)], by = c("Track2" = "Track2"))
master_class<-master_class %>%
filter(cluster2 != 0)
master_class$cluster2<-as.numeric(master_class$cluster2)
master_class_sum<-master_class%>%group_by(position,class,mouse, Track2)%>%
arrange(Time)%>%summarize(cluster2=mean(cluster2), V1=mean(V1), V2=mean(V2),
dist_tumor=mean(dist_tumor), dist_3_neigh=mean(dist_3_neigh),
dist_10_neigh=mean(dist_10_neigh), speed=mean(speed), disp2=mean(disp2),
disp_d=mean(disp_d),disp_l=mean(disp_l), movement=last(dist_tumor),
distance_to_tumor=last(dist_tumor_1),raw_dist_tumor=last(dist_tumor),
Time=first(Time))%>%ungroup()
## Join the information on the SR101 and MG
master_class_sum <- left_join(master_class_sum,master_distance_MG[c("Track2","n_MG","min_MG")] ,by = c("Track2" = "Track2"))
master_class_sum <- left_join(master_class_sum,master_distance_SR101[c("Track2","n_SR101","min_SR101")] ,by = c("Track2" = "Track2"))
master_class_sum <- left_join(master_class_sum,BV_df_sum ,by = c("Track2" = "Track2"))
mid <- 0
master_class_sum$movement2<-ifelse(master_class_sum$movement>0,"away from tumor", ifelse(master_class_sum$movement==0, "no movement", "towards the tumor"))
master_class_sum<-master_class_sum[!is.na(master_class_sum$cluster2),]
saveRDS(master_class_sum,"master_class_sum.rds")
write.csv(master_class_sum,"master_class_sum.csv")
And here is the resulting plot:
ggplot(master_class_sum, mapping=aes(x=V1, y=V2, color=movement)) +
geom_point(size=2, alpha=0.8) +labs(color="Direction")+
xlab("") + ylab("") +
ggtitle("Direction of movement relative to tumor core") +
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+theme(aspect.ratio=1)+
scale_color_gradient2(midpoint = mid, low = "blue",mid="grey80",
high = "red3")
In this section, we project the relevant clustering features in a Heatmap
###Create heatmap
sum_all<-master_class_sum%>%group_by(cluster2)%>%
summarise(displacement2 = median(disp2),
speed = median(speed),displacement_delta=median(disp_d),displacement_length=median(disp_l),max_speed=quantile(speed, 0.75), persistance=(displacement_length/displacement_delta),nearest_10_cell= median(dist_10_neigh),dist_tumor_core=median(distance_to_tumor),
n_SR101=mean(n_SR101, na.rm = T),
min_SR101=mean(min_SR101, na.rm = T),n_MG=mean(n_MG, na.rm = T),min_MG=mean(min_MG, na.rm = T),min_BV_dist=mean(BV_min, na.rm = T),sd_BV_dist=mean(BV_sd, na.rm = T),mean_BV_dist=mean(BV_mean, na.rm = T),
movement=median(movement) )
Cluster_movement<-sum_all[,c(1,length(names(sum_all)))]
Cluster_movement <-Cluster_movement[order(Cluster_movement$movement),]
stdize = function(x, ...) {(x - min(x, ...)) / (max(x, ...) - min(x, ...))}
sum_all[-c(1)] <- lapply(sum_all[-c(1)], stdize, na.rm = T)
df<-sum_all[,-c(1)]
df<-df[order(match(rownames(df), Cluster_movement$cluster2)), , drop = FALSE]%>%ungroup()
df<-as.data.frame(df)
rownames(df)<-Cluster_movement$cluster2
We show all the features extracted in the analysis:
heat_m<-pheatmap(df, clustering_method = "complete", cluster_cols=F,cluster_rows=F,cellwidth=20, cellheight=20,
treeheight_row = 0, treeheight_col = 0,fontsize = 8,na_col = "grey50", main="Features",
angle_col = 90,color = colorRampPalette(c("deepskyblue1", "grey95", "deeppink2"))(50))
df_cl_features<-df[,c("displacement2", "speed", "max_speed", "displacement_delta" , "displacement_length", "persistance", "movement")]
rownames(df_cl_features)<-rownames(df)
And a heatmap of the features used for clustering:
pheatmap(df_cl_features, clustering_method = "complete", cluster_cols=F,cluster_rows=F,cellwidth=20, cellheight=20,
treeheight_row = 0, treeheight_col = 0,fontsize = 8,na_col = "grey50", main="Features used for clustering",
angle_col = 90,color = colorRampPalette(c("deepskyblue1", "grey95", "deeppink2"))(50))
We also print tha Cluster movement data (towards or away from the
tumor):
print(Cluster_movement) #3 per cluster we can see the direction
## # A tibble: 7 × 2
## cluster2 movement
## <dbl> <dbl>
## 1 6 -8.13
## 2 5 -2.14
## 3 1 -0.962
## 4 7 -0.537
## 5 3 0.995
## 6 4 1.61
## 7 2 6.34
Plots the clusters according to direction and other relevant
features
## Movement within the clusters in UMAP
## plot clusters giving them a color according to direction
# Convert df to a data frame
# Add cluster2 as a column
df1<-as.data.frame(df)
df1 <- mutate(df1, cluster2 = row.names(df))
# Order df1 by movement and reorder cluster2 accordingly
df1 <- df1 %>%
arrange(movement) %>%
mutate(cluster2 = factor(cluster2, levels = unique(cluster2)))
##set first order clusters based on the heatmap:
UMAP representation with the behavioral clusters:
ggplot(master_class_sum, aes(x = V1, y = V2, color = as.factor(cluster2))) +
geom_point(size = 2, alpha = 1) +
labs(color = "Cluster") +
xlab("") +
ylab("") +
scale_color_manual(values = setNames(mypalette_1, levels(df1$cluster2))) + # Match palette to cluster2 levels ggtitle("UMAP Cluster") +
theme_light(base_size = 20) +
theme(axis.text.x = element_blank(),
axis.text.y = element_blank(),
aspect.ratio = 1)
UMAP representation with the behavioral clusters among mice:
ggplot(umap_3, aes(x=V1, y=V2, color=as.factor(cluster2))) +
geom_point(size=1, alpha=0.8) + labs(color="cluster")+
xlab("") + ylab("")+
ggtitle("umap Cluster among mice") +scale_color_manual(values = setNames(mypalette_1, levels(df1$cluster2))) +
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+facet_wrap(mouse~.)
UMAP representation with the localization bistribution of each
track:
ggplot(master_class_sum, mapping=aes(x=V1, y=V2, color=distance_to_tumor)) +
geom_point(size=2, alpha=0.6) +
xlab("") + ylab("") +
ggtitle("UMAP localization distribution") +
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+
scale_color_gradientn(colours = rev(RColorBrewer::brewer.pal(4, "Spectral")))
UMAP representation with the raw movement of each track:
ggplot(master_class_sum, mapping=aes(x=V1, y=V2, color=movement)) +
geom_point(size=2, alpha=0.6) +
xlab("") + ylab("") +
ggtitle("UMAP raw_movement") +
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+
scale_color_gradientn(colours = rev(RColorBrewer::brewer.pal(4, "Spectral")))
UMAP representation with the speed distribution of each track:
ggplot(master_class_sum, mapping=aes(x=V1, y=V2, color=speed)) +
geom_point(size=2, alpha=0.6) +
xlab("") + ylab("") +
ggtitle("UMAP speed distribution") +
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+
scale_color_gradientn(colours = rev(RColorBrewer::brewer.pal(4, "Spectral")))
UMAP representation with the squared displacement disitribution of each
track:
ggplot(master_class_sum, mapping=aes(x=V1, y=V2, color=disp2)) +
geom_point(size=2, alpha=0.6) +
xlab("") + ylab("") +
ggtitle("UMAP disp2distribution") +
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+
scale_color_gradientn(colours = rev(RColorBrewer::brewer.pal(4, "Spectral")))
UMAP representation with the displacement length of each track:
ggplot(master_class_sum, mapping=aes(x=V1, y=V2, color=disp_l)) +
geom_point(size=2, alpha=0.6) +
xlab("") + ylab("") +
ggtitle("UMAP disp_length") +
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+
scale_color_gradientn(colours = rev(RColorBrewer::brewer.pal(4, "Spectral")))
Provides a pie chart of the cluster distribution along the mice
experiments
### plot enrichment on edge and core:
Number_cell_exp<-umap_3%>%group_by(position,class,mouse)%>%
summarise(total_cell = n())
Percentage_clus<-left_join(Number_cell_exp,umap_3)
Percentage_clus <- Percentage_clus%>%group_by(cluster2,position,class,mouse)%>%
summarise(total_cell = mean(total_cell), num_cluster=n())%>%mutate(percentage=num_cluster*100/total_cell)%>%ungroup()
Percentage_clus_2 <- Percentage_clus%>%group_by(cluster2, position,class,mouse)%>%summarise(se_percentage=sd(percentage)/sqrt(length(percentage)),percentage = mean(percentage))
### Plot circular map
Percentage_clus_2$cluster2<-as.factor(Percentage_clus_2$cluster2)
Percentage_clus_2$cluster2<- factor(Percentage_clus_2$cluster2, levels = levels(df1$cluster2))
Behavioral cluster distribution in each mouse
Per2<-ggplot(Percentage_clus_2, aes(fill=as.factor(cluster2), y=percentage, x="")) +
geom_bar( stat="identity", position="fill", width = 0.5)+ coord_flip()+ scale_y_reverse()
Per2 <- Per2 + facet_grid(.~mouse)
Per2<-Per2+theme_void()+ scale_fill_manual(values=mypalette_1)+theme(strip.text.x = element_text(size = 8, angle = 90))
Per2
Mouse distribution in eacch behavioral cluster
Per4<-ggplot(Percentage_clus_2, aes(fill=as.factor(mouse), y=percentage, x="")) +
geom_bar( stat="identity", position="fill", width = 0.5)+ coord_flip()+ scale_y_reverse()
Per4 <- Per4 + facet_grid(cluster2~.)
Per4<-Per4+theme_void()+theme(strip.text.x = element_text(size = 8, angle = 90))
Per4
This section analyzes the differences between clusters for different features.
### plot differences of the values per cluster
master_class_sum<-as.data.frame(master_class_sum)
master_class_sum$cluster2<-as.character(master_class_sum$cluster2)
df1$mean_movement<-df1$movement
master_class_sum2<-left_join(master_class_sum, df1[,c("cluster2","mean_movement")])
master_class_sum2$cluster2 = factor(master_class_sum2$cluster2, levels=df1$cluster2)
p_speed <- ggplot(master_class_sum2,aes(x=as.factor(cluster2) , y=speed, group=cluster2,fill=cluster2)) +geom_jitter(width=0.2, alpha=0.5) +
geom_boxplot(outlier.colour = NA, alpha=0.5) +ggtitle("speed per cluster")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(0,20))
p_direction <- ggplot(master_class_sum2,aes(x=as.factor(cluster2) , y=movement, group=cluster2,fill=cluster2)) +geom_jitter(width=0.2, alpha=0.5) +
geom_boxplot(outlier.colour = NA, alpha=0.5) +ggtitle("speed per cluster")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(-15,20))
p_dist3_neigth <- ggplot(master_class_sum2,aes(x=as.factor(cluster2) , y=dist_3_neigh, group=cluster2, fill=cluster2)) +geom_jitter(width=0.2, alpha=0.5) +
geom_boxplot(alpha=0.5, outlier.colour = NA)+ggtitle("dist 3 neigh per cluster")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(10,70))
p_dist_10_neigh <- ggplot(master_class_sum2,aes(x=as.factor(cluster2) , y=dist_10_neigh, group=cluster2,fill=cluster2)) +geom_jitter(width=0.2, alpha=0.5) +
geom_boxplot( outlier.colour = NA, alpha=0.5) +ggtitle("dist 10 neigh per cluster")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(20,90))
p_min_MG <- ggplot(subset(master_class_sum2, !is.na(min_MG)) ,aes(x=as.factor(cluster2) , y=min_MG, group=cluster2, fill=cluster2)) +geom_jitter(width=0.3, alpha=0.5) +
geom_boxplot( outlier.colour = NA, alpha=0.5) +ggtitle("distance to closest MG")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(2,45))
p_min_SR101 <- ggplot(subset(master_class_sum2, !is.na(min_SR101)) ,aes(x=as.factor(cluster2) , y=min_SR101, group=cluster2, fill=cluster2)) +geom_jitter(width=0.3, alpha=0.5) +
geom_boxplot(outlier.colour = NA, alpha=0.5) +ggtitle("distance to closest SR101")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(0,50))
p_n_MG <- ggplot(subset(master_class_sum2, !is.na(n_MG)) ,aes(x=as.factor(cluster2) , y=n_MG, group=cluster2, fill=cluster2)) +geom_jitter(width=0.3, alpha=0.5) +
geom_boxplot( outlier.colour = NA, alpha=0.5) +ggtitle("n MG")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(0,15))
p_nSR101 <- ggplot(subset(master_class_sum2, !is.na(n_SR101)) ,aes(x=as.factor(cluster2) , y=n_SR101, group=cluster2, fill=cluster2)) +geom_jitter(width=0.3, alpha=0.5) +
geom_boxplot(outlier.colour = NA, alpha=0.5) +ggtitle("n SR101")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(0,20))
p_BV_mean <- ggplot(subset(master_class_sum2, !is.na(BV_mean)) ,aes(x=as.factor(cluster2) , y=BV_mean, group=cluster2, fill=cluster2)) +geom_jitter(width=0.3, alpha=0.5) +
geom_boxplot(outlier.colour = NA, alpha=0.5) +ggtitle("BV_mean")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(0,20))
p_BV_min <- ggplot(subset(master_class_sum2, !is.na(BV_min)) ,aes(x=as.factor(cluster2) , y=BV_min, group=cluster2, fill=cluster2)) +geom_jitter(width=0.2, alpha=0.5) +
geom_boxplot( outlier.colour = NA, alpha=0.5) +ggtitle("BV_min")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(0,20))
p_BV_contact <- ggplot(subset(master_class_sum2, !is.na(BV_min)) ,aes(x=as.factor(cluster2) , y=BV_contact, group=cluster2, fill=cluster2)) +geom_jitter(width=0.3, alpha=0.5) +
geom_boxplot(outlier.colour = NA, alpha=0.5) +ggtitle("BV_contact")+theme_classic()+ scale_fill_manual(values=mypalette_1)+
theme(aspect.ratio=0.7)
p_BV_sd <- ggplot(subset(master_class_sum2, !is.na(BV_min)) ,aes(x=as.factor(cluster2) , y=BV_sd, group=cluster2, fill=cluster2)) +geom_jitter(width=0.3, alpha=0.5) +
geom_boxplot( outlier.colour = NA, alpha=0.5) +ggtitle("Standart deviation distance")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(0,1.7))
The output in this section shows a boxplot for each feature in each
behavioral cluster
p_speed
p_direction
p_dist3_neigth
p_dist_10_neigh
p_min_MG
p_min_SR101
p_n_MG
p_nSR101
p_BV_mean
p_BV_min
p_BV_contact
p_BV_sd
Select a position of interest to save .txt information
about the specified position of interest in a .txt
file.
This code section uses the master_distance.rds file
generated in the File Import section
# Determine position of interest
position_interest<-"2430F11.CL3_1"
names_cell<-master_class[!duplicated(master_class$mouse),]
names_cell$position
## [1] 2430F11.CL3_1 2430F13.CL2_2 2430F16.CL3_1 2430F17.CL3_2 2430F18.CL2_1
## [6] 2430F12.CL2_2
## 36 Levels: 2430F11.CL1_1 2430F12.CL1_1 2430F13.CL1_1 ... 2430F18.CL3_2
Tracks_1<-master_class[which(master_class$position==position_interest),]
Tracks_1<-Tracks_1[!duplicated(Tracks_1$Track2),c(2,which( colnames(master_class)=="cluster2" ))]
##Join with information on unique TrackID for that experiment
# Generated during the data import
master_distance<-readRDS("D:/BEHAV3D_Tumor_Profiler/results/master_distance.rds")
master_distance<-master_distance[c("TrackID", "Track2")]%>% distinct(TrackID, .keep_all = TRUE)
Tracks_1<-left_join(Tracks_1,master_distance)
Tracks_1$Track2<-NULL
Track_1_list<-split(Tracks_1,Tracks_1$cluster2)
write(paste(as.character(Track_1_list), sep="' '", collapse=", "), paste0(position_interest,".txt"))
Then, we plot the cell trajectories combined with the behavioral cluster information:
###Plot cell trajectories:
## bind cluster information data to original dataset:
Tracks_1<-master_class
###plot all the tracks
Tracks_1<-Tracks_1[!duplicated(Tracks_1$Track2),c("cluster2", "Track2")]
Tracks_1$cluster2<-as.factor(Tracks_1$cluster2)
master_traject<-left_join(master_cor2,Tracks_1)
master_traject<-na.omit(master_traject)
master_traject<-subset(master_traject, cluster2!=0)
master_traject<-master_traject%>%group_by(position, mouse)%>%mutate(x_new=x-min(x), y_new=y-min(y))
master_traject$cluster2 = factor(master_traject$cluster2, levels=df1$cluster2)
g_all_track<-ggplot(master_traject)+geom_path(aes(x_new, y_new, group=Track2,
color=as.factor(cluster2)), size=0.2,
arrow = arrow(ends = "last",type = "open",length = unit(0.01, "inches")))+scale_color_manual(values=mypalette_1 )+
theme_bw()+theme(aspect.ratio = 1)+facet_wrap(class~position)
g_all_track
g_track_int1<-ggplot(subset(master_traject, position%in%position_interest))+geom_path(aes(x_new, y_new, group=Track2, color=as.factor(cluster2)), size=0.5,arrow = arrow(ends = "last",type = "open",length = unit(0.00, "inches")))+
scale_color_manual(values=mypalette_1)+ggtitle(paste0(position_interest))+
theme_bw()+theme(aspect.ratio = 1)
g_track_int1
g_track_int2<-ggplot(subset(master_traject, position%in%position_interest))+geom_path(aes(x_new, y_new, group=Track2), size=0.5,arrow = arrow(ends = "last",type = "open",length = unit(0.00, "inches")))+
scale_color_manual(values=mypalette_1)+ggtitle(paste0(position_interest))+
theme_bw()+theme(aspect.ratio = 1)
g_track_int2
Dynamic Time Warping (DTW) is an algorithm used to measure similarity between two time series by aligning them in a way that minimizes the distance between corresponding points. It is particularly useful for comparing time series that may vary in speed or timing, like on this case.
If the matrix_distmat.rds file exists, it will skip the
DTW analysis time, as it has been run previously. Else, it will run
normally and generate the file at the end.
###MULTIVARIATE
list_master_pca <- split(master_pca3[,-c(1)],master_pca3$Track2) ## split into list
if ( ! file.exists("matrix_distmat.rds")){
##parallel working
# load parallel
library(parallel)
# create multi-process workers
workers <- makeCluster(detectCores()-2)
# load dtwclust in each one, and make them use 1 thread per worker
invisible(clusterEvalQ(workers, {
library(dtwclust)
RcppParallel::setThreadOptions(1L)
}))
# register your workers, e.g. with doParallel
require(doParallel)
registerDoParallel(workers)
###MULTIVARIATE
set.seed(123)
distmat <- proxy::dist(list_master_pca, method = "dtw")
saveRDS(distmat, file = "distmat.rds")
matrix_distmat<-as.matrix(distmat)
saveRDS(matrix_distmat, file = "matrix_distmat.rds")
}else{
matrix_distmat <- readRDS("matrix_distmat.rds")
}
Track2<-as.numeric(names(list_master_pca))
Now, we can perform the UMAP representation and cluster in the desired number of clusters. Any of the parameters can be modified to your convenience
# Modify the parameters according to the characteristics of the experiment
umap_dist<- umap(matrix_distmat,n_components=2,input="dist",init = "random",
n_neighbors=7,min_dist=0.5, spread=6,n_epochs=1000 , local_connectivity=1)
umap_1 <- as.data.frame(umap_dist$`layout`)
Here, we can see the Raw UMAP plot:
plot(umap_dist$`layout`, # Plot the result
col=rgb(0,0,0,alpha=0.1),
pch=19,
asp=0.4, xlab="UMAP 1", ylab="UMAP 2",
main="Raw UMAP")
Here we determine the number of clusters for the analysis.
(n_cluster)
umap_1 <- as.data.frame(umap_dist$`layout`)
Track2_umap<-cbind(Track2,umap_1)
positiontype<-master_norm[,c("Track2", "position","class","mouse")]
positiontype<- positiontype[!duplicated(positiontype$Track2),]
umap_2 <- left_join(Track2_umap ,positiontype)
# Clustering
n_cluster=7
hc <- hclust(dist(umap_dist$`layout`, method = "euclidean"), method = "ward.D2")
hierarchical_clusters <- cutree(hc, k = n_cluster)
umap_3 <- cbind(hierarchical_clusters, umap_2)
colnames(umap_3)[1]<- "cluster2"
UMAP positions colored by large-scale regions
ggplot(umap_3, aes(x=V1, y=V2, color=as.factor(class))) +
geom_point(size=0.5, alpha=0.8) + labs(color="Environment_cluster")+
xlab("") + ylab("")+
ggtitle("umap Cluster") +scale_color_manual(values=c("cyan","gold","red"))+
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+facet_wrap(mouse~position)
UMAP colored by large-scale regions
ggplot(umap_3, aes(x=V1, y=V2, color=as.factor(class))) +
geom_point(size=2, alpha=0.6) + labs(color="Tumor region phenotype")+
xlab("") + ylab("")+
ggtitle("distribution of large scale regions")+scale_color_manual(values=c("cyan","gold","red"))+
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)
### calculate average stats per track
master_class <- left_join(master_cor2 ,umap_3[c(1:4)], by = c("Track2" = "Track2"))
master_class<-master_class %>%
filter(cluster2 != 0)
master_class$cluster2<-as.numeric(master_class$cluster2)
master_class_sum<-master_class%>%group_by(position,class,mouse, Track2)%>%
arrange(Time)%>%summarize(cluster2=mean(cluster2), V1=mean(V1), V2=mean(V2),
dist_tumor=mean(dist_tumor), dist_3_neigh=mean(dist_3_neigh),
dist_10_neigh=mean(dist_10_neigh), speed=mean(speed), disp2=mean(disp2),
disp_d=mean(disp_d),disp_l=mean(disp_l), movement=last(dist_tumor),
distance_to_tumor=last(dist_tumor_1),raw_dist_tumor=last(dist_tumor),
Time=first(Time))%>%ungroup()
## Join the information on the SR101 and MG
master_class_sum <- left_join(master_class_sum,master_distance_MG[c("Track2","n_MG","min_MG")] ,by = c("Track2" = "Track2"))
master_class_sum <- left_join(master_class_sum,master_distance_SR101[c("Track2","n_SR101","min_SR101")] ,by = c("Track2" = "Track2"))
master_class_sum <- left_join(master_class_sum,BV_df_sum ,by = c("Track2" = "Track2"))
mid <- 0
master_class_sum$movement2<-ifelse(master_class_sum$movement>0,"away from tumor", ifelse(master_class_sum$movement==0, "no movement", "towards the tumor"))
master_class_sum<-master_class_sum[!is.na(master_class_sum$cluster2),]
saveRDS(master_class_sum,"master_class_sum.rds")
write.csv(master_class_sum,"master_class_sum.csv")
Provides a pie chart of the cluster distribution along the mice experiments
sum_all<-master_class_sum%>%group_by(cluster2)%>%
summarise(displacement2 = median(disp2),
speed = median(speed),displacement_delta=median(disp_d),displacement_length=median(disp_l),max_speed=quantile(speed, 0.75), persistance=(displacement_length/displacement_delta),nearest_10_cell= median(dist_10_neigh),dist_tumor_core=median(distance_to_tumor),
n_SR101=mean(n_SR101, na.rm = T),
min_SR101=mean(min_SR101, na.rm = T),n_MG=mean(n_MG, na.rm = T),min_MG=mean(min_MG, na.rm = T),min_BV_dist=mean(BV_min, na.rm = T),sd_BV_dist=mean(BV_sd, na.rm = T),mean_BV_dist=mean(BV_mean, na.rm = T),
movement=median(movement) )
Cluster_movement<-sum_all[,c(1,length(names(sum_all)))]
Cluster_movement <-Cluster_movement[order(Cluster_movement$movement),]
stdize = function(x, ...) {(x - min(x, ...)) / (max(x, ...) - min(x, ...))}
sum_all[-c(1)] <- lapply(sum_all[-c(1)], stdize, na.rm = T)
df<-sum_all[,-c(1)]
df<-df[order(match(rownames(df), Cluster_movement$cluster2)), , drop = FALSE]%>%ungroup()
df<-as.data.frame(df)
rownames(df)<-Cluster_movement$cluster2
## plot clusters giving them a color according to direction
# Convert df to a data frame
# Add cluster2 as a column
df1<-as.data.frame(df)
df1 <- mutate(df1, cluster2 = row.names(df))
# Order df1 by movement and reorder cluster2 accordingly
df1 <- df1 %>%
arrange(movement) %>%
mutate(cluster2 = factor(cluster2, levels = unique(cluster2)))
# Make a color palette that adjusts to the cluster direction
# Generate a continuous palette with 100 colors
continuous_palette <- colorRampPalette(c("cyan4", "darkturquoise","darkorchid4","darkorchid1", "deeppink4","deeppink1", "goldenrod3", "gold"))(50)
# Select 8 colors from the continuous palette
mypalette_1<- continuous_palette[seq(1, length(continuous_palette), length.out = n_cluster)]
### plot enrichment on edge and core:
Number_cell_exp<-umap_3%>%group_by(position,class,mouse)%>%
summarise(total_cell = n())
Percentage_clus<-left_join(Number_cell_exp,umap_3)
Percentage_clus <- Percentage_clus%>%group_by(cluster2,position,class,mouse)%>%
summarise(total_cell = mean(total_cell), num_cluster=n())%>%mutate(percentage=num_cluster*100/total_cell)%>%ungroup()
Percentage_clus_2 <- Percentage_clus%>%group_by(cluster2, position,class,mouse)%>%summarise(se_percentage=sd(percentage)/sqrt(length(percentage)),percentage = mean(percentage))
Plots the pie chart of:
### Plot circular map
Percentage_clus_2$cluster2<-as.factor(Percentage_clus_2$cluster2)
Percentage_clus_2$cluster2<- factor(Percentage_clus_2$cluster2, levels = levels(df1$cluster2))
Per1<-ggplot(Percentage_clus_2, aes(fill=as.factor(cluster2), y=percentage, x="")) +
geom_bar( stat="identity", position="fill", width = 0.5)+ coord_flip()+ scale_y_reverse()
Per1 <- Per1 + facet_grid(class~mouse)
Per1<-Per1+theme_void()+ scale_fill_manual(values=mypalette_1)+theme(strip.text.x = element_text(size = 8, angle = 90))
Per1
- Behavioral cluster distribution in large-scale regions
Per3<-ggplot(Percentage_clus_2, aes(fill=as.factor(cluster2), y=percentage, x="")) +
geom_bar( stat="identity", position="fill", width = 0.5)+ coord_flip()+ scale_y_reverse()
Per3 <- Per3 + facet_grid(class~.)
Per3<-Per3+theme_void()+theme(strip.text.x = element_text(size = 8, angle = 90))+ scale_fill_manual(values=mypalette_1)
Per3
# Additional Statistics
# Create an empty dataframe to store results
results_df <- data.frame(cluster2 = character(), p_value = numeric(),contrast=character(), pairwise_p_values=numeric())
# Create an empty list to store plots
plots_list <- list()
# Iterate over unique clusters
for (cluster in unique(Percentage_clus_2$cluster2)) {
# Subset data for the current cluster
cluster_data <- subset(Percentage_clus_2, cluster2 == cluster)
# Calculate mean percentage for each mouse and subtract from original percentage
normalized_data <- cluster_data %>%
group_by(mouse) %>%
mutate(normalized_percentage = (percentage - mean(percentage)) / sd(percentage)) %>%
ungroup()
# Fit model with normalized percentage values
model <- lm(normalized_percentage ~ class, data = normalized_data)
# Extract estimates and p-value from model summary
model_summary <- summary(model)
p_value <- anova(model)$`Pr(>F)`[1]
# Conduct pairwise comparisons using emmeans
pairwise_comp <- emmeans(model, pairwise ~ class, adjust = "tukey")
# Extract pairwise comparisons and p-values
pairwise_p_values <- summary(pairwise_comp)[["contrasts"]][["p.value"]]
contrast<-summary(pairwise_comp)[["contrasts"]][["contrast"]]
# Append cluster name, estimates, and p-value to results dataframe
results_df <- rbind(results_df, data.frame(cluster2 = cluster, p_value = p_value, contrast=contrast,pairwise_p_values=pairwise_p_values))
# Create boxplot of normalized percentages by class
plot <- ggplot(normalized_data, aes(x = class, y = normalized_percentage)) +
geom_boxplot(width=0.5) +geom_jitter(width = 0.3)+
labs(title = paste("Cluster:", cluster)) +
theme_bw()+theme(aspect.ratio = 1)
# Store plot in the list
plots_list[[cluster]] <- plot
}
Display the results dataframe:
# Display the results dataframe
print(results_df)
## cluster2 p_value contrast pairwise_p_values
## 1 1 0.6467245 CL1 - CL2 0.7541313
## 2 1 0.6467245 CL1 - CL3 0.6534765
## 3 1 0.6467245 CL2 - CL3 0.9843553
## 4 2 0.1263177 CL1 - CL2 0.4110613
## 5 2 0.1263177 CL1 - CL3 0.1098208
## 6 2 0.1263177 CL2 - CL3 0.6749548
## 7 3 0.2518502 CL1 - CL2 0.8263790
## 8 3 0.2518502 CL1 - CL3 0.2398153
## 9 3 0.2518502 CL2 - CL3 0.4942696
## 10 4 0.2370106 CL1 - CL2 0.4103133
## 11 4 0.2370106 CL1 - CL3 0.9059262
## 12 4 0.2370106 CL2 - CL3 0.2350897
## 13 5 0.2568905 CL1 - CL2 0.3853240
## 14 5 0.2568905 CL1 - CL3 0.2589728
## 15 5 0.2568905 CL2 - CL3 0.9480454
## 16 6 0.2822381 CL1 - CL2 0.4004833
## 17 6 0.2822381 CL1 - CL3 0.9936312
## 18 6 0.2822381 CL2 - CL3 0.3152953
## 19 7 0.7861878 CL1 - CL2 0.9645045
## 20 7 0.7861878 CL1 - CL3 0.9092402
## 21 7 0.7861878 CL2 - CL3 0.7689239
Plot the statistics in large-scale regions (environmental
clusters)
plots_list
## $`1`
##
## $`2`
##
## $`3`
##
## $`4`
##
## $`5`
##
## $`6`
##
## $`7`
This section analyzes the differences between large-scale regions for different features.
####Differences based on environmental clusters
### plot differences of the values per cluster
master_class_sum<-as.data.frame(master_class_sum)
master_class_sum$cluster2<-as.character(master_class_sum$cluster2)
df1$mean_movement<-df1$movement
master_class_sum2<-left_join(master_class_sum, df1[,c("cluster2","mean_movement")])
master_class_sum2$cluster2 = factor(master_class_sum2$cluster2, levels=df1$cluster2)
#### try an anova between clusters based on distance to speed
p_class_speed <- ggplot(master_class_sum2,aes(x=as.factor(class) , y=speed, group=class,fill=class)) +
geom_violin(alpha=1, outlier.colour = NA)+stat_summary(fun = median, geom = "crossbar", size = 1, color = "grey25")+scale_fill_manual(values=c("cyan","gold","red"))+ggtitle("speed per env cluster")+theme_classic()+
theme(aspect.ratio=1)+coord_cartesian(ylim = c(0,20))
p_class_disp2 <- ggplot(master_class_sum2,aes(x=as.factor(class) , y=disp2, group=class,fill=class)) +
geom_violin(alpha=1, outlier.colour = NA)+stat_summary(fun = median, geom = "crossbar", size = 1, color = "grey25")+scale_fill_manual(values=c("cyan","gold","red"))+ggtitle("speed per env cluster")+theme_classic()+
theme(aspect.ratio=1)+coord_cartesian(ylim = c(0,180))
p_class_movement <- ggplot(master_class_sum2,aes(x=as.factor(class) , y=movement, group=class,fill=class)) +
geom_violin(alpha=1, outlier.colour = NA)+stat_summary(fun = median, geom = "crossbar", size = 1, color = "grey25")+scale_fill_manual(values=c("cyan","gold","red")) +ggtitle("movement direction per env cluster")+theme_classic()+
theme(aspect.ratio=1)+scale_color_gradientn(colours = rev(RColorBrewer::brewer.pal(4, "Spectral")))+coord_cartesian(ylim = c(-10,17))
The output in this section shows a boxplot for each feature in each behavioral cluster according to large-scale regions:
p_class_speed
p_class_disp2
p_class_movement
Dynamic Time Warping (DTW) is an algorithm used to measure similarity between two time series by aligning them in a way that minimizes the distance between corresponding points. It is particularly useful for comparing time series that may vary in speed or timing, like on this case.
If the matrix_distmat.rds file exists, it will skip the
DTW analysis time, as it has been run previously. Else, it will run
normally and generate the file at the end.
###MULTIVARIATE
list_master_pca <- split(master_pca3[,-c(1)],master_pca3$Track2) ## split into list
if ( ! file.exists("matrix_distmat.rds")){
##parallel working
# load parallel
library(parallel)
# create multi-process workers
workers <- makeCluster(detectCores()-2)
# load dtwclust in each one, and make them use 1 thread per worker
invisible(clusterEvalQ(workers, {
library(dtwclust)
RcppParallel::setThreadOptions(1L)
}))
# register your workers, e.g. with doParallel
require(doParallel)
registerDoParallel(workers)
###MULTIVARIATE
set.seed(123)
distmat <- proxy::dist(list_master_pca, method = "dtw")
saveRDS(distmat, file = "distmat.rds")
matrix_distmat<-as.matrix(distmat)
saveRDS(matrix_distmat, file = "matrix_distmat.rds")
}else{
matrix_distmat <- readRDS("matrix_distmat.rds")
}
Track2<-as.numeric(names(list_master_pca))
Now, we can perform the UMAP representation and cluster in the desired number of clusters. Any of the parameters can be modified to your convenience.
However, in this module, the output of the UMAP is not relevant, as it does not reflect any small-scale phenotype.
# Adjust parameters according to the characteristics of the experiment
umap_dist<- umap(matrix_distmat,n_components=2,input="dist",init = "random",
n_neighbors=7,min_dist=0.5, spread=6,n_epochs=1000 , local_connectivity=1)
umap_1 <- as.data.frame(umap_dist$`layout`)
Track2_umap<-cbind(Track2,umap_1)
positiontype<-master_norm[,c("Track2", "position","class","mouse")]
positiontype<- positiontype[!duplicated(positiontype$Track2),]
umap_2 <- left_join(Track2_umap ,positiontype)
# Clustering
set.seed(123)
n_cluster=7
hc <- hclust(dist(umap_dist$`layout`, method = "euclidean"), method = "ward.D2")
hierarchical_clusters <- cutree(hc, k = n_cluster)
umap_3 <- cbind(hierarchical_clusters, umap_2)
colnames(umap_3)[1]<- "cluster2"
### calculate average stats per track
master_class <- left_join(master_cor2 ,umap_3[c(1:4)], by = c("Track2" = "Track2"))
master_class<-master_class %>%
filter(cluster2 != 0)
master_class$cluster2<-as.numeric(master_class$cluster2)
master_class_sum<-master_class%>%group_by(position,class,mouse, Track2)%>%
arrange(Time)%>%summarize(cluster2=mean(cluster2), V1=mean(V1), V2=mean(V2),
dist_tumor=mean(dist_tumor), dist_3_neigh=mean(dist_3_neigh),
dist_10_neigh=mean(dist_10_neigh), speed=mean(speed), disp2=mean(disp2),
disp_d=mean(disp_d),disp_l=mean(disp_l), movement=last(dist_tumor),
distance_to_tumor=last(dist_tumor_1),raw_dist_tumor=last(dist_tumor),
Time=first(Time))%>%ungroup()
## Join the information on the SR101 and MG
master_class_sum <- left_join(master_class_sum,master_distance_MG[c("Track2","n_MG","min_MG")] ,by = c("Track2" = "Track2"))
master_class_sum <- left_join(master_class_sum,master_distance_SR101[c("Track2","n_SR101","min_SR101")] ,by = c("Track2" = "Track2"))
master_class_sum <- left_join(master_class_sum,BV_df_sum ,by = c("Track2" = "Track2"))
mid <- 0
master_class_sum$movement2<-ifelse(master_class_sum$movement>0,"away from tumor", ifelse(master_class_sum$movement==0, "no movement", "towards the tumor"))
master_class_sum<-master_class_sum[!is.na(master_class_sum$cluster2),]
saveRDS(master_class_sum,"master_class_sum.rds")
write.csv(master_class_sum,"master_class_sum.csv")
In this section, we project the relevant small-scale features in a Heatmap
###Create heatmap
sum_all<-master_class_sum%>%group_by(cluster2)%>%
summarise(displacement2 = median(disp2),
speed = median(speed),displacement_delta=median(disp_d),displacement_length=median(disp_l),max_speed=quantile(speed, 0.75), persistance=(displacement_length/displacement_delta),nearest_10_cell= median(dist_10_neigh),dist_tumor_core=median(distance_to_tumor),
n_SR101=mean(n_SR101, na.rm = T),
min_SR101=mean(min_SR101, na.rm = T),n_MG=mean(n_MG, na.rm = T),min_MG=mean(min_MG, na.rm = T),min_BV_dist=mean(BV_min, na.rm = T),sd_BV_dist=mean(BV_sd, na.rm = T),mean_BV_dist=mean(BV_mean, na.rm = T),
movement=median(movement) )
Cluster_movement<-sum_all[,c(1,length(names(sum_all)))]
Cluster_movement <-Cluster_movement[order(Cluster_movement$movement),]
stdize = function(x, ...) {(x - min(x, ...)) / (max(x, ...) - min(x, ...))}
sum_all[-c(1)] <- lapply(sum_all[-c(1)], stdize, na.rm = T)
df<-sum_all[,-c(1)]
df<-df[order(match(rownames(df), Cluster_movement$cluster2)), , drop = FALSE]%>%ungroup()
df<-as.data.frame(df)
rownames(df)<-Cluster_movement$cluster2
Heatmap of all the included features:
heat_m<-pheatmap(df, clustering_method = "complete", cluster_cols=F,cluster_rows=F,cellwidth=20, cellheight=20,
treeheight_row = 0, treeheight_col = 0,fontsize = 8,na_col = "grey50", main=" All features",
angle_col = 90,color = colorRampPalette(c("deepskyblue1", "grey95", "deeppink2"))(50))
df_other_features<-df[,!colnames(df)%in%c("displacement2", "speed", "max_speed", "displacement_delta" , "displacement_length", "persistance", "movement")]
rownames(df_other_features)<-rownames(df)
Heatmap of the small-scale features:
pheatmap(df_other_features, clustering_method = "complete", cluster_cols=F,cluster_rows=F,cellwidth=20, cellheight=20,
treeheight_row = 0, treeheight_col = 0,fontsize = 8,na_col = "grey50",main="Small-scale TME features",
angle_col = 90,color = colorRampPalette(c("deepskyblue1", "grey95", "deeppink2"))(50))
Plots the behavioral clusters according to relevant small-scale
features
## plot clusters giving them a color according to direction
# Convert df to a data frame
# Add cluster2 as a column
df1<-as.data.frame(df)
df1 <- mutate(df1, cluster2 = row.names(df))
# Order df1 by movement and reorder cluster2 accordingly
df1 <- df1 %>%
arrange(movement) %>%
mutate(cluster2 = factor(cluster2, levels = unique(cluster2)))
##set first order clusters based on the heatmap:
ggplot(master_class_sum, mapping=aes(x=V1, y=V2, color=n_SR101)) +
geom_point(size=2, alpha=0.6) +
xlab("") + ylab("") +
ggtitle("UMAP number of SR101 in 30um radius distribution") +
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+
scale_color_gradientn(colours = rev(RColorBrewer::brewer.pal(4, "Spectral")),na.value="NA")
ggplot(master_class_sum, mapping=aes(x=V1, y=V2, color=min_SR101)) +
geom_point(size=2, alpha=0.6) +
xlab("") + ylab("") +
ggtitle("UMAP min distance to SR101 in 30um radius distribution") +
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+
scale_color_gradientn(colours = RColorBrewer::brewer.pal(4, "Spectral"),na.value="NA", trans="pseudo_log")
ggplot(master_class_sum, mapping=aes(x=V1, y=V2, color=min_MG)) +
geom_point(size=2, alpha=0.6) +
xlab("") + ylab("") +
ggtitle("UMAP min distance to MG in 30um radius distribution") +
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+
scale_color_gradientn(colours = RColorBrewer::brewer.pal(4, "Spectral"),na.value="NA", trans="pseudo_log")
ggplot(master_class_sum, mapping=aes(x=V1, y=V2, color=n_MG)) +
geom_point(size=2, alpha=0.6) +
xlab("") + ylab("") +
ggtitle("UMAP number of MG in 30um radius distribution") +
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+
scale_color_gradientn(colours = rev(RColorBrewer::brewer.pal(4, "Spectral")),na.value="NA")
ggplot(master_class_sum, mapping=aes(x=V1, y=V2, color=BV_mean)) +
geom_point(size=2, alpha=0.6) +
xlab("") + ylab("") +
ggtitle("UMAP mean distance to Blood vessels") +
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+
scale_color_gradientn(colours = rev(RColorBrewer::brewer.pal(4, "Spectral")),na.value="NA", trans="pseudo_log")
ggplot(master_class_sum, mapping=aes(x=V1, y=V2, color=BV_min)) +
geom_point(size=2, alpha=0.6) +
xlab("") + ylab("") +
ggtitle("UMAP mean contact with Blood vessels") +
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+
scale_color_gradientn(colours = rev(RColorBrewer::brewer.pal(4, "Spectral")),na.value="NA")
ggplot(master_class_sum, mapping=aes(x=V1, y=V2, color=dist_10_neigh)) +
geom_point(size=2, alpha=0.6) +
xlab("") + ylab("") +
ggtitle("UMAP big neighbours distribution") +
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+
scale_color_gradientn(colours = rev(RColorBrewer::brewer.pal(4, "Spectral")),trans="pseudo_log")
ggplot(master_class_sum, mapping=aes(x=V1, y=V2, color=dist_3_neigh)) +
geom_point(size=2, alpha=0.6) +
xlab("") + ylab("") +
ggtitle("UMAP close neighbours distribution") +
theme_light(base_size=20) +
theme(axis.text.x=element_blank(),
axis.text.y=element_blank())+theme(aspect.ratio=1)+
scale_color_gradientn(colours = rev(RColorBrewer::brewer.pal(4, "Spectral")),trans="pseudo_log")
This section analyzes the differences between clusters for small-scale features.
### plot differences of the values per cluster
master_class_sum<-as.data.frame(master_class_sum)
master_class_sum$cluster2<-as.character(master_class_sum$cluster2)
df1$mean_movement<-df1$movement
master_class_sum2<-left_join(master_class_sum, df1[,c("cluster2","mean_movement")])
master_class_sum2$cluster2 = factor(master_class_sum2$cluster2, levels=df1$cluster2)
p_dist3_neigth <- ggplot(master_class_sum2,aes(x=as.factor(cluster2) , y=dist_3_neigh, group=cluster2, fill=cluster2)) +geom_jitter(width=0.2, alpha=0.5) +
geom_boxplot(alpha=0.5, outlier.colour = NA)+ggtitle("dist 3 neigh per cluster")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(10,70))
p_dist_10_neigh <- ggplot(master_class_sum2,aes(x=as.factor(cluster2) , y=dist_10_neigh, group=cluster2,fill=cluster2)) +geom_jitter(width=0.2, alpha=0.5) +
geom_boxplot( outlier.colour = NA, alpha=0.5) +ggtitle("dist 10 neigh per cluster")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(20,90))
p_min_MG <- ggplot(subset(master_class_sum2, !is.na(min_MG)) ,aes(x=as.factor(cluster2) , y=min_MG, group=cluster2, fill=cluster2)) +geom_jitter(width=0.3, alpha=0.5) +
geom_boxplot( outlier.colour = NA, alpha=0.5) +ggtitle("distance to closest MG")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(2,45))
p_min_SR101 <- ggplot(subset(master_class_sum2, !is.na(min_SR101)) ,aes(x=as.factor(cluster2) , y=min_SR101, group=cluster2, fill=cluster2)) +geom_jitter(width=0.3, alpha=0.5) +
geom_boxplot(outlier.colour = NA, alpha=0.5) +ggtitle("distance to closest SR101")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(0,50))
p_n_MG <- ggplot(subset(master_class_sum2, !is.na(n_MG)) ,aes(x=as.factor(cluster2) , y=n_MG, group=cluster2, fill=cluster2)) +geom_jitter(width=0.3, alpha=0.5) +
geom_boxplot( outlier.colour = NA, alpha=0.5) +ggtitle("n MG")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(0,15))
p_nSR101 <- ggplot(subset(master_class_sum2, !is.na(n_SR101)) ,aes(x=as.factor(cluster2) , y=n_SR101, group=cluster2, fill=cluster2)) +geom_jitter(width=0.3, alpha=0.5) +
geom_boxplot(outlier.colour = NA, alpha=0.5) +ggtitle("n SR101")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(0,20))
p_BV_mean <- ggplot(subset(master_class_sum2, !is.na(BV_mean)) ,aes(x=as.factor(cluster2) , y=BV_mean, group=cluster2, fill=cluster2)) +geom_jitter(width=0.3, alpha=0.5) +
geom_boxplot(outlier.colour = NA, alpha=0.5) +ggtitle("BV_mean")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(0,20))
p_BV_min <- ggplot(subset(master_class_sum2, !is.na(BV_min)) ,aes(x=as.factor(cluster2) , y=BV_min, group=cluster2, fill=cluster2)) +geom_jitter(width=0.2, alpha=0.5) +
geom_boxplot( outlier.colour = NA, alpha=0.5) +ggtitle("BV_min")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(0,20))
p_BV_contact <- ggplot(subset(master_class_sum2, !is.na(BV_min)) ,aes(x=as.factor(cluster2) , y=BV_contact, group=cluster2, fill=cluster2)) +geom_jitter(width=0.3, alpha=0.5) +
geom_boxplot(outlier.colour = NA, alpha=0.5) +ggtitle("BV_contact")+theme_classic()+ scale_fill_manual(values=mypalette_1)+
theme(aspect.ratio=0.7)
p_BV_sd <- ggplot(subset(master_class_sum2, !is.na(BV_min)) ,aes(x=as.factor(cluster2) , y=BV_sd, group=cluster2, fill=cluster2)) +geom_jitter(width=0.3, alpha=0.5) +
geom_boxplot( outlier.colour = NA, alpha=0.5) +ggtitle("Standart deviation distance")+theme_classic()+
theme(aspect.ratio=0.7)+ scale_fill_manual(values=mypalette_1)+coord_cartesian(ylim = c(0,1.7))
The output in this section shows a boxplot for each small-scale feature
in each behavioral cluster
p_dist3_neigth
p_dist_10_neigh
p_min_MG
p_min_SR101
p_n_MG
p_nSR101
p_BV_mean
p_BV_min
p_BV_contact
p_BV_sd
This section analyzes the differences between large-scale regions for small-scale features.
####Differences based on environmental clusters
### plot differences of the values per cluster
master_class_sum<-as.data.frame(master_class_sum)
master_class_sum$cluster2<-as.character(master_class_sum$cluster2)
df1$mean_movement<-df1$movement
master_class_sum2<-left_join(master_class_sum, df1[,c("cluster2","mean_movement")])
master_class_sum2$cluster2 = factor(master_class_sum2$cluster2, levels=df1$cluster2)
p_class_dist_10neigh <- ggplot(master_class_sum2,aes(x=as.factor(class) , y=dist_10_neigh, fill=class)) +
geom_violin(alpha=1, outlier.colour = NA)+stat_summary(fun = median, geom = "crossbar", size = 1, color = "grey25")+scale_fill_manual(values=c("cyan","gold","red")) +ggtitle("dist 10 neigh per cluster")+theme_classic()+
theme(aspect.ratio=1)+coord_cartesian(ylim = c(20,70))
p_class_minMG <- ggplot(subset(master_class_sum2, !is.na(min_MG)) ,aes(x=as.factor(class) , y=min_MG, fill=class)) +
geom_violin(alpha=1, outlier.colour = NA)+stat_summary(fun = median, geom = "crossbar", size = 1, color = "grey25")+scale_fill_manual(values=c("cyan","gold","red")) +ggtitle("distance to closest MG")+theme_classic()+
theme(aspect.ratio=1)+coord_cartesian(ylim = c(0,50))
p_classmin_SR101 <- ggplot(subset(master_class_sum2, !is.na(min_SR101)) ,aes(x=as.factor(class) , y=min_SR101, fill=class)) +
geom_violin(alpha=1, outlier.colour = NA) +stat_summary(fun = median, geom = "crossbar", size = 1, color = "grey25")+scale_fill_manual(values=c("cyan","gold","red"))+ggtitle("distance to closest SR101")+theme_classic()+
theme(aspect.ratio=1)+coord_cartesian(ylim = c(0,50))
p_class_n_MG <- ggplot(subset(master_class_sum2, !is.na(min_MG)),aes(x=as.factor(class) , y=n_MG, fill=class)) +
geom_violin(alpha=1, outlier.colour = NA) +stat_summary(fun = median, geom = "crossbar", size = 1, color = "grey25")+scale_fill_manual(values=c("cyan","gold","red"))+ggtitle("n of MG per cluster")+theme_classic()+
theme(aspect.ratio=1)
p_classn_SR101 <- ggplot(subset(master_class_sum2, !is.na(min_SR101)),aes(x=as.factor(class) , y=n_SR101, fill=class)) +
geom_violin(alpha=1, outlier.colour = NA)+stat_summary(fun = median, geom = "crossbar", size = 1, color = "grey25")+scale_fill_manual(values=c("cyan","gold","red"))+ggtitle("n SR101 per cluster")+theme_classic()+
theme(aspect.ratio=1)
p_class_BV_min <- ggplot(subset(master_class_sum2, !is.na(BV_min)) ,aes(x=as.factor(class) , y=BV_min, fill=class)) +
geom_violin(alpha=1, outlier.colour = NA)+stat_summary(fun = median, geom = "crossbar", size = 1, color = "grey25")+ scale_fill_manual(values=c("cyan","gold","red")) +ggtitle("min distance to BV")+theme_classic()+
theme(aspect.ratio=1)+coord_cartesian(ylim = c(0,18))
p_class_BV_mean <- ggplot(subset(master_class_sum2, !is.na(BV_mean)) ,aes(x=as.factor(class) , y=BV_mean, fill=class)) +
geom_violin(alpha=1, outlier.colour = NA)+stat_summary(fun = median, geom = "crossbar", size = 1, color = "grey25")+ scale_fill_manual(values=c("cyan","gold","red")) +ggtitle("mean distance to BV")+theme_classic()+
theme(aspect.ratio=1)+coord_cartesian(ylim = c(0,18))
pcor_MG_position <- ggplot(subset(master_class_sum2, !is.na(min_MG)),aes(x=distance_to_tumor , y=n_MG)) +geom_jitter()+
geom_point(alpha=0.5,stat = "summary") +geom_smooth(method = "lm")+ggtitle("scatter plot movement vs min_MG")+theme_classic()+
theme(aspect.ratio=1)
The output in this section shows a boxplot for each feature in each behavioral cluster according to large-scale regions:
p_class_dist_10neigh
p_class_minMG
p_classmin_SR101
p_class_n_MG
p_classn_SR101
p_class_BV_min
p_class_BV_mean
## R version 4.4.3 (2025-02-28 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26100)
##
## Matrix products: default
##
##
## Random number generation:
## RNG: L'Ecuyer-CMRG
## Normal: Inversion
## Sample: Rejection
##
## locale:
## [1] LC_COLLATE=Spanish_Spain.utf8 LC_CTYPE=Spanish_Spain.utf8
## [3] LC_MONETARY=Spanish_Spain.utf8 LC_NUMERIC=C
## [5] LC_TIME=Spanish_Spain.utf8
##
## time zone: Europe/Madrid
## tzcode source: internal
##
## attached base packages:
## [1] parallel stats graphics grDevices utils datasets methods
## [8] base
##
## other attached packages:
## [1] doParallel_1.0.17 iterators_1.0.14 foreach_1.5.2
## [4] zoo_1.8-13 viridis_0.6.5 viridisLite_0.4.2
## [7] umap_0.2.10.0 tidyr_1.3.1 spatstat_3.3-1
## [10] spatstat.linnet_3.2-5 spatstat.model_3.3-4 rpart_4.1.24
## [13] spatstat.explore_3.3-4 nlme_3.1-167 spatstat.random_3.3-2
## [16] spatstat.geom_3.3-5 spatstat.univar_3.1-1 spatstat.data_3.1-4
## [19] sp_2.2-0 scales_1.3.0 reshape2_1.4.4
## [22] readr_2.1.5 RANN_2.6.2 plotly_4.10.4
## [25] pheatmap_1.0.12 gtools_3.9.5 ggplot2_3.5.2
## [28] emmeans_1.10.7 e1071_1.7-16 dtwclust_6.0.0
## [31] dtw_1.23-1 proxy_0.4-27 dplyr_1.1.4
## [34] DT_0.33
##
## loaded via a namespace (and not attached):
## [1] deldir_2.0-4 gridExtra_2.3 rlang_1.1.6
## [4] magrittr_2.0.3 clue_0.3-66 flexclust_1.5.0
## [7] compiler_4.4.3 mgcv_1.9-1 png_0.1-8
## [10] vctrs_0.6.5 stringr_1.5.1 crayon_1.5.3
## [13] pkgconfig_2.0.3 fastmap_1.2.0 labeling_0.4.3
## [16] promises_1.3.2 rmarkdown_2.29 tzdb_0.4.0
## [19] bit_4.5.0.1 purrr_1.0.4 xfun_0.52
## [22] modeltools_0.2-23 cachem_1.1.0 jsonlite_1.9.1
## [25] goftest_1.2-3 later_1.4.1 spatstat.utils_3.1-2
## [28] cluster_2.1.8 R6_2.6.1 bslib_0.9.0
## [31] stringi_1.8.4 RColorBrewer_1.1-3 reticulate_1.41.0
## [34] jquerylib_0.1.4 estimability_1.5.1 Rcpp_1.0.14
## [37] knitr_1.49 tensor_1.5 splines_4.4.3
## [40] httpuv_1.6.15 Matrix_1.7-2 tidyselect_1.2.1
## [43] rstudioapi_0.17.1 abind_1.4-8 yaml_2.3.10
## [46] codetools_0.2-20 lattice_0.22-6 tibble_3.2.1
## [49] plyr_1.8.9 shiny_1.10.0 withr_3.0.2
## [52] askpass_1.2.1 coda_0.19-4.1 evaluate_1.0.3
## [55] RcppParallel_5.1.10 polyclip_1.10-7 pillar_1.10.1
## [58] stats4_4.4.3 shinyjs_2.1.0 generics_0.1.3
## [61] vroom_1.6.5 hms_1.1.3 munsell_0.5.1
## [64] xtable_1.8-4 class_7.3-23 glue_1.8.0
## [67] lazyeval_0.2.2 tools_4.4.3 data.table_1.17.0
## [70] RSpectra_0.16-2 mvtnorm_1.3-3 grid_4.4.3
## [73] crosstalk_1.2.1 colorspace_2.1-1 cli_3.6.4
## [76] spatstat.sparse_3.1-0 gtable_0.3.6 sass_0.4.9
## [79] digest_0.6.37 ggrepel_0.9.6 farver_2.1.2
## [82] htmlwidgets_1.6.4 htmltools_0.5.8.1 lifecycle_1.0.4
## [85] httr_1.4.7 mime_0.12 bit64_4.6.0-1
## [88] openssl_2.3.2
Thank you for reading the BEHAV3D Tumor Profiler - Guided Tutorial.